﻿

#region Using Statements

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Threading;
using System.ComponentModel;
using System.Reflection;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Design;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

using Freeform;
using FGame = Freeform.Game;
using XGame = Microsoft.Xna.Framework.Game;
using FComponent = Freeform.Component;
using FViewport = Freeform.Viewport;

#endregion


namespace Freeform
{
    /// <summary>
    /// Class description.
    /// </summary>
    public class Surface3D : Entity3D
    {

        #region Events

        #endregion


        #region Members

        public Int32 Rows { get; set; }

        public Int32 Columns { get; set; }

        public VertexPositionColorNormal[] HeightMap { get; set; }

        public Int32[] Adjacency { get; set; }

        public VertexPositionColor[] pickedTriangle =
        {
            new VertexPositionColor(Vector3.Zero, Color.Magenta),
            new VertexPositionColor(Vector3.Zero, Color.Magenta),
            new VertexPositionColor(Vector3.Zero, Color.Magenta),
        };

        public VertexPositionColor[] mouseRayVerts =
        {
            new VertexPositionColor(Vector3.Zero, Color.Orange),
            new VertexPositionColor(Vector3.Zero, Color.Orange),
        };

        private Effect Effect { get; set; }

        private BasicEffect LineEffect { get; set; }



        public Vector3 LightDirection 
        { 
            get 
            { 
                return new Vector3(-_LightDirection.X, -_LightDirection.Z, -_LightDirection.Y); 
            } 
            set 
            { 
                value.Normalize(); 
                _LightDirection = new Vector3(-value.X, -value.Z, -value.Y); 
            } 
        }
        private Vector3 _LightDirection;

        public Matrix WorldMatrix { get; private set; }

        #endregion


        #region Init

        public Surface3D(World World, Int32 Rows, Int32 Columns)
            : base(World)
        {
            // Setup surface values.
            this.Rows = Rows;
            this.Columns = Columns;
            HeightMap = new VertexPositionColorNormal[Rows * Columns];
            LightDirection = -Vector3.One;

            // Setup constant effect settings.
            Effect = Game.Content.Load<Effect>("Effects/BasicShader");
            Effect.CurrentTechnique = Effect.Techniques["Colored"];
            Effect.Parameters["xEnableLighting"].SetValue(true);         
            Effect.Parameters["xAmbient"].SetValue(0.1f);

            // Setup constant effect settings.
            LineEffect = new BasicEffect(this.Game.GraphicsDevice);
            LineEffect.VertexColorEnabled = true;

            // Start geometry calculations.
            CreateFlatSurface(0);
            UpdateGeometry(false);
        }

        #endregion


        #region Update

        #endregion


        #region Draw

        public override void Draw(GameTime Time, FViewport Viewport)
        {
            base.Draw(Time, Viewport);

            // Only render inside 3d viewports.
            Viewport3D View = Viewport as Viewport3D;
            if (View == null) return;

            Vector3 Scaling = new Vector3(Size.X / Rows, Size.Y / Columns, 1);

            // Setup world matrix for the surface.
            WorldMatrix = 
                Matrix.CreateRotationX(Orientation.X) *
                Matrix.CreateRotationY(Orientation.Y) *
                Matrix.CreateRotationZ(Orientation.Z) *
                Matrix.CreateScale(Scaling) *
                Matrix.CreateTranslation(Position);

            // Set graphics device to linear clamp the Surface.
            Game.GraphicsDevice.SamplerStates[0] = SamplerState.LinearClamp;

            // Set terrain effect settings.
            Effect.Parameters["xLightDirection"].SetValue(_LightDirection);
            Effect.Parameters["xView"].SetValue(View.Camera.ViewMatrix);
            Effect.Parameters["xProjection"].SetValue(View.Camera.ProjectionMatrix);
            Effect.Parameters["xWorld"].SetValue(WorldMatrix);

            //Execute Effect passes and draw the terrain.
            foreach (EffectPass Pass in Effect.CurrentTechnique.Passes)
            {
                Pass.Apply();
                Game.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, HeightMap, 0, HeightMap.Length, Adjacency, 0, Adjacency.Length / 3, Freeform.VertexPositionColorNormal.VertexDeclaration);
            }

            // Activate the line drawing BasicEffect.
            LineEffect.Projection = View.Camera.ProjectionMatrix;
            LineEffect.View = View.Camera.ViewMatrix;

            LineEffect.CurrentTechnique.Passes[0].Apply();

            // Draw the triangle.
            Game.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
                                      pickedTriangle, 0, 1);

            Game.GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineList, mouseRayVerts, 0, 1);
            
            // Restore sampler states for model rendering.
            Game.GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
        }

        #endregion


        #region Dispose

        #endregion


        #region Utility

        public Single GetHeight(Vector2 Coordinates)
        {
            Int32 X = (Int32)((Coordinates.X - Position.X) / (Size.X) * Rows);
            Int32 Y = (Int32)((Coordinates.Y - Position.Y) / (Size.Y) * Columns);

            if (X < 0 || Y < 0) return 0;
            if (X >= Rows || Y >= Columns) return 0;

            return HeightMap[X + Y * Rows].Position.Z;
        }

        public void UpdateGeometry(Boolean AutoColoring)
        {
            if (AutoColoring)
                SetupColours();
            SetupAdjacency();
            SetupNormals();
        }


        public void CreateFlatSurface(Single Height)
        {
            for (Int32 x = 0; x < Rows; x++)
                for (Int32 y = 0; y < Columns; y++)
                {
                    HeightMap[x + y * Rows].Position = new Vector3(new Vector2(x, y), Height);
                    HeightMap[x + y * Rows].Color = Color.CornflowerBlue;
                }
        }

        public void LoadHeightFromTexture(Texture2D Texture, Boolean AdjustVertexCount, Single MaxHeight, Single MinHeight)
        {
            Int32 ImageWidth = Texture.Width;
            Int32 ImageHeight = Texture.Height;

            Single HeightScale = (MaxHeight - MinHeight) / 255f;

            if (AdjustVertexCount)
            {
                Rows = ImageWidth;
                Columns = ImageHeight;
                HeightMap = new VertexPositionColorNormal[Rows * Columns];
            }

            Single PixelsPerRow = ImageWidth / Rows;
            Single PixelsPerCol = ImageHeight / Columns;

            // Extract color information from image.
            Color[] ImageData = new Color[ImageWidth * ImageHeight];
            Texture.GetData(ImageData);

            // Set surface heights.
            for (Int32 x = 0; x < Rows; x++)
            {
                for (Int32 y = 0; y < Columns; y++)
                {
                    Int32 ImageX = (Int32)(x * PixelsPerRow);
                    Int32 ImageY = (Int32)(y * PixelsPerCol);

                    Single Height = ImageData[ImageX + ImageY * ImageWidth].R * HeightScale + MinHeight;
                    HeightMap[x + y * Rows].Position = new Vector3(new Vector2(x, y), Height);
                }
            }

            UpdateGeometry(true);
        }

        public Single HeightAt(Vector2 Coordinates, out int Cord)
        {
            int Coord = 0;
            Coord = ((int)(Coordinates.X / Size.X) + (int)(Coordinates.Y / Size.Y) * (Rows));
            Cord = Coord;
            if (Coord < 0 || Coord > 65536)
                return 0f;

            return HeightMap[Coord].Position.Z;
        }

        #endregion


        #region Implementation

        private void SetupColours()
        {
            Single Min = Single.MaxValue;
            Single Max = Single.MinValue;

            for (int x = 0; x < Rows; x++)
            {
                for (int y = 0; y < Columns; y++)
                {
                    if (HeightMap[x + y * Rows].Position.Z < Min)
                        Min = HeightMap[x + y * Rows].Position.Z;
                    if (HeightMap[x + y * Rows].Position.Z > Max)
                        Max = HeightMap[x + y * Rows].Position.Z;
                }
            }

            for (int x = 0; x < Rows; x++)
            {
                for (int y = 0; y < Columns; y++)
                {
                    //Sets color based on height from ground. To "simulate" lakes/mountains.
                    if (HeightMap[x + y * Rows].Position.Z < Min + (Max - Min) * 0.25f)
                        HeightMap[x + y * Rows].Color = Color.Blue;
                    else if (HeightMap[x + y * Rows].Position.Z < Min + (Max - Min) * 0.5f)
                        HeightMap[x + y * Rows].Color = Color.Green;
                    else if (HeightMap[x + y * Rows].Position.Z < Min + (Max - Min) * 0.75f)
                        HeightMap[x + y * Rows].Color = Color.Brown;
                    else
                        HeightMap[x + y * Rows].Color = Color.White;
                }
            }
        }

        private void SetupAdjacency()
        {
            Adjacency = new Int32[(Rows - 1) * (Columns - 1) * 6];
            Int32 Index = 0;
            for (Int32 x = 0; x < Rows - 1; x++)
                for (Int32 y = 0; y < Rows - 1; y++)
                {
                    Int32 TL = ((x) + ((y) * Rows));
                    Int32 TR = ((x + 1) + ((y) * Rows));
                    Int32 BL = ((x) + ((y + 1) * Rows));
                    Int32 BR = ((x + 1) + ((y + 1) * Rows));

                    // Form bottom left triangle.
                    Adjacency[Index++] = TL;
                    Adjacency[Index++] = BL;
                    Adjacency[Index++] = BR;

                    // Form top right triangle.
                    Adjacency[Index++] = TL;
                    Adjacency[Index++] = TR;
                    Adjacency[Index++] = BR;
                }
        }


        private void SetupNormals()
        {
            Int32 i = 0;
            for (i = 0; i < HeightMap.Length; i++)
                HeightMap[i].Normal = Vector3.Zero;

            i = 0;
            while (i < Adjacency.Length)
            {
                int A = Adjacency[i++];
                int B = Adjacency[i++];
                int C = Adjacency[i++];

                Vector3 AC = HeightMap[C].Position - HeightMap[A].Position;
                Vector3 AB = HeightMap[B].Position - HeightMap[A].Position;
                Vector3 Normal = Vector3.Cross(AC, AB);

                HeightMap[A].Normal = Normal;
                HeightMap[B].Normal = Normal;
                HeightMap[C].Normal = Normal;
            }

            for (i = 0; i < HeightMap.Length; i++)
                HeightMap[i].Normal.Normalize();
        }

        #endregion

    }


    public struct VertexPositionColorNormal
    {
        //XNA does not include a type for storing Position/colour/normal.
        //It has a Position/Colour type but we needed to make a new one for the normal.
        //Is used inside this class and by the DrawUserPrimitives/DrawIndexedPrimitives draw calls.
        public Vector3 Position;
        public Color Color;
        public Vector3 Normal;

        public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration
        (
            new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
            new VertexElement(sizeof(Single) * 3, VertexElementFormat.Color, VertexElementUsage.Color, 0),
            new VertexElement(sizeof(Single) * 3 + 4, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0)
        );
    }
}
